/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
 * file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
 * to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
 * License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by
 * applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */

package org.apache.log4j;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;

import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.spi.LoggingEvent;

/**
 * DailyRollingFileAppender extends {@link FileAppender} so that the underlying file is rolled over at a user chosen
 * frequency.
 * <p>
 * The rolling schedule is specified by the <b>DatePattern</b> option. This pattern should follow the
 * {@link SimpleDateFormat} conventions. In particular, you <em>must</em> escape literal text within a pair of single
 * quotes. A formatted version of the date pattern is used as the suffix for the rolled file name.
 * <p>
 * For example, if the <b>File</b> option is set to <code>/foo/bar.log</code> and the <b>DatePattern</b> set to
 * <code>'.'yyyy-MM-dd</code>, on 2001-02-16 at midnight, the logging file <code>/foo/bar.log</code> will be copied to
 * <code>/foo/bar.log.2001-02-16</code> and logging for 2001-02-17 will continue in <code>/foo/bar.log</code> until it
 * rolls over the next day.
 * <p>
 * Is is possible to specify monthly, weekly, half-daily, daily, hourly, or minutely rollover schedules.
 * <p>
 * <table border="1" cellpadding="2">
 * <tr>
 * <th>DatePattern</th>
 * <th>Rollover schedule</th>
 * <th>Example</th>
 * <tr>
 * <td><code>'.'yyyy-MM</code>
 * <td>Rollover at the beginning of each month</td>
 * <td>At midnight of May 31st, 2002 <code>/foo/bar.log</code> will be copied to <code>/foo/bar.log.2002-05</code>.
 * Logging for the month of June will be output to <code>/foo/bar.log</code> until it is also rolled over the next
 * month.
 * <tr>
 * <td><code>'.'yyyy-ww</code>
 * <td>Rollover at the first day of each week. The first day of the week depends on the locale.</td>
 * <td>Assuming the first day of the week is Sunday, on Saturday midnight, June 9th 2002, the file <i>/foo/bar.log</i>
 * will be copied to <i>/foo/bar.log.2002-23</i>. Logging for the 24th week of 2002 will be output to
 * <code>/foo/bar.log</code> until it is rolled over the next week.
 * <tr>
 * <td><code>'.'yyyy-MM-dd</code>
 * <td>Rollover at midnight each day.</td>
 * <td>At midnight, on March 8th, 2002, <code>/foo/bar.log</code> will be copied to <code>/foo/bar.log.2002-03-08</code>
 * . Logging for the 9th day of March will be output to <code>/foo/bar.log</code> until it is rolled over the next day.
 * <tr>
 * <td><code>'.'yyyy-MM-dd-a</code>
 * <td>Rollover at midnight and midday of each day.</td>
 * <td>At noon, on March 9th, 2002, <code>/foo/bar.log</code> will be copied to <code>/foo/bar.log.2002-03-09-AM</code>.
 * Logging for the afternoon of the 9th will be output to <code>/foo/bar.log</code> until it is rolled over at midnight.
 * <tr>
 * <td><code>'.'yyyy-MM-dd-HH</code>
 * <td>Rollover at the top of every hour.</td>
 * <td>At approximately 11:00.000 o'clock on March 9th, 2002, <code>/foo/bar.log</code> will be copied to
 * <code>/foo/bar.log.2002-03-09-10</code>. Logging for the 11th hour of the 9th of March will be output to
 * <code>/foo/bar.log</code> until it is rolled over at the beginning of the next hour.
 * <tr>
 * <td><code>'.'yyyy-MM-dd-HH-mm</code>
 * <td>Rollover at the beginning of every minute.</td>
 * <td>At approximately 11:23,000, on March 9th, 2001, <code>/foo/bar.log</code> will be copied to
 * <code>/foo/bar.log.2001-03-09-10-22</code>. Logging for the minute of 11:23 (9th of March) will be output to
 * <code>/foo/bar.log</code> until it is rolled over the next minute.
 * </table>
 * <p>
 * Do not use the colon ":" character in anywhere in the <b>DatePattern</b> option. The text before the colon is
 * interpeted as the protocol specificaion of a URL which is probably not what you want.
 *
 * @author Eirik Lygre
 * @author Ceki G&uuml;lc&uuml;
 * @version $Id: $
 */
public class DailyRollingCompressingFileAppender extends DailyRollingFileAppender
{
  public static enum TYPE
  {
    // BZIP,
    GZIP, DEFLATE
  }

  private TYPE type = TYPE.GZIP;

  interface CompressionWriter
  {

    void write(byte[] buf, int off, int len) throws IOException;

    void close() throws IOException;
  }

  abstract class Writer implements CompressionWriter
  {

    private FileOutputStream outputstream;
    private int              buffersize;

    /**
     * @param destination
     * @param buffSize
     */
    public Writer(final FileOutputStream destination, final int buffSize)
    {
      setOutputstream(destination);
      setBuffersize(buffSize);
    }

    /**
     * @param outputstream
     *          the outputstream to set
     */
    public void setOutputstream(final FileOutputStream outputstream)
    {
      this.outputstream = outputstream;
    }

    /**
     * @return the outputstream
     */
    public FileOutputStream getOutputstream()
    {
      return outputstream;
    }

    /**
     * @param buffersize
     *          the buffersize to set
     */
    public void setBuffersize(final int buffersize)
    {
      this.buffersize = buffersize;
    }

    /**
     * @return the buffersize
     */
    public int getBuffersize()
    {
      return buffersize;
    }

    abstract public void close() throws IOException;

    /*
     * (non-Javadoc)
     * @see org.apache.log4j.DailyRollingCompressingFileAppender.CompressionWriter#write(byte[], int, int)
     */
    abstract public void write(byte[] buf, int off, int len) throws IOException;
  }

  // class BZIPWriter extends Writer
  // {
  // private final CBZip2OutputStream outputstream;
  //
  // public BZIPWriter(FileOutputStream destination, int buffSize) throws
  // IOException
  // {
  // super(destination, buffSize);
  //
  // outputstream = new CBZip2OutputStream(destination);
  // }
  //
  // /* (non-Javadoc)
  // * @see
  // org.apache.log4j.DailyRollingCompressingFileAppender.CompressionWriter#write(byte[],
  // int, int)
  // */
  // @Override
  // public void write(byte[] buf, int off, int len) throws IOException
  // {
  // outputstream.write(buf, off, len);
  // }
  //
  // @Override
  // public void close() throws IOException
  // {
  // outputstream.close();
  // }
  // }

  class GZIPWriter extends Writer
  {
    private final GZIPOutputStream outputstream;

    public GZIPWriter(final FileOutputStream destination, final int buffSize) throws IOException
    {
      super(destination, buffSize);

      outputstream = new GZIPOutputStream(destination, buffSize);
    }

    /*
     * (non-Javadoc)
     * @see org.apache.log4j.DailyRollingCompressingFileAppender.CompressionWriter#write(byte[], int, int)
     */
    @Override
    public void write(final byte[] buf, final int off, final int len) throws IOException
    {
      outputstream.write(buf, off, len);
    }

    @Override
    public void close() throws IOException
    {
      outputstream.close();
    }
  }

  class DeflateWriter extends Writer
  {
    private final DeflaterOutputStream outputstream;

    public DeflateWriter(final FileOutputStream destination, final int buffSize) throws IOException
    {
      super(destination, buffSize);

      Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
      outputstream = new DeflaterOutputStream(destination, deflater, buffSize);
    }

    /*
     * (non-Javadoc)
     * @see org.apache.log4j.DailyRollingCompressingFileAppender.CompressionWriter#write(byte[], int, int)
     */
    @Override
    public void write(final byte[] buf, final int off, final int len) throws IOException
    {
      outputstream.write(buf, off, len);
    }

    /*
     * (non-Javadoc)
     * @see org.apache.log4j.DailyRollingCompressingFileAppender.Writer#close()
     */
    @Override
    public void close() throws IOException
    {
      outputstream.close();
    }
  }

  /**
   * The log file will be renamed to the value of the scheduledFilename variable when the next interval is entered. For
   * example, if the rollover period is one hour, the log file will be renamed to the value of "scheduledFilename" at
   * the beginning of the next hour. The precise time when a rollover occurs depends on logging activity.
   */
  private String     scheduledFilename;

  /**
   * The next time we estimate a rollover should occur.
   */
  private final long nextCheck = System.currentTimeMillis() - 1;

  /**
   * The default constructor does nothing.
   */
  public DailyRollingCompressingFileAppender()
  {
  }

  /**
   * Instantiate a <code>DailyRollingFileAppender</code> and open the file designated by <code>filename</code>. The
   * opened filename will become the ouput destination for this appender.
   *
   * @param layout a {@link org.apache.log4j.Layout} object.
   * @param filename a {@link java.lang.String} object.
   * @param datePattern a {@link java.lang.String} object.
   * @throws java.io.IOException if any.
   */
  public DailyRollingCompressingFileAppender(final Layout layout, final String filename, final String datePattern) throws IOException
  {
    super(layout, filename, datePattern);
    activateOptions();
  }

  /** {@inheritDoc} */
  @Override
  public void activateOptions()
  {
    super.activateOptions();
    if ((getDatePattern() != null) && (fileName != null))
    {
      now.setTime(System.currentTimeMillis());
      sdf = new SimpleDateFormat(getDatePattern());
      int type = computeCheckPeriod();
      printPeriodicity(type);
      rc.setType(type);
      File file = new File(fileName);
      scheduledFilename = fileName + sdf.format(new Date(file.lastModified()));

    }
    else
    {
      LogLog.error("Either File or DatePattern options are not set for appender [" + name + "].");
    }
  }

  /**
   * Rollover the current file to a new file.
   */
  @Override
  void rollOver() throws IOException
  {

    /* Compute filename, but only if datePattern is specified */
    if (getDatePattern() == null)
    {
      errorHandler.error("Missing DatePattern option in rollOver().");
      return;
    }

    String datedFilename = fileName + sdf.format(now);
    // It is too early to roll over because we are still within the
    // bounds of the current interval. Rollover will occur once the
    // next interval is reached.

    if (scheduledFilename.equals(datedFilename))
    {
      return;
    }

    // close current file, and rename it to datedFilename
    this.closeFile();

    final File target = new File(scheduledFilename);
    if (target.exists())
    {
      target.delete();
    }

    File file = new File(fileName);
    boolean result = file.renameTo(target);

    Thread t = new Thread(new Runnable()
    {
      public void run()
      {
        try
        {
          compressFile(target + "");
        }
        catch (Exception e1)
        {
          e1.printStackTrace();
        }
      }
    });

    // t.setPriority(Thread.MIN_PRIORITY);
    // t.setDaemon(true);
    t.start();

    if (result)
    {
      LogLog.debug(fileName + " -> " + scheduledFilename);
    }
    else
    {
      LogLog.error("Failed to rename [" + fileName + "] to [" + scheduledFilename + "].");
    }

    try
    {
      // This will also close the file. This is OK since multiple
      // close operations are safe.
      this.setFile(fileName, false, this.bufferedIO, this.bufferSize);
    }
    catch (IOException e)
    {
      errorHandler.error("setFile(" + fileName + ", false) call failed.");
    }
    scheduledFilename = datedFilename;
  }

  /**
   * <p>compressFile.</p>
   *
   * @param source a {@link java.io.File} object.
   * @param destination a {@link java.io.File} object.
   * @throws java.lang.Exception if any.
   */
  public void compressFile(final File source, final File destination) throws Exception
  {
    System.err.println("starting compressing with " + type + " " + source.getAbsolutePath());
    // if (destination.exists())
    // {
    // throw new Exception("destination file already exists:" +
    // destination.getAbsolutePath());
    // }
    if (!source.exists())
    {
      throw new Exception("source file doesnt exist:" + source.getAbsolutePath());
    }

    int buffSize = 9 * 1024 * 100;

    InputStream inputStream = new FileInputStream(source);

    // Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION, true);

    // GZIPOutputStream outputStream = new GZIPOutputStream(new
    // FileOutputStream(destination), buffSize);

    CompressionWriter writer = compressionFactory(destination, buffSize, type);

    byte[] buf = new byte[buffSize];
    int len;

    while ((len = inputStream.read(buf)) > 0)
    {
      writer.write(buf, 0, len);
    }

    inputStream.close();
    writer.close();

    // System.out.println("old size: " + source.length());
    // System.out.println("new size: " + destination.length());

    if (source.exists())
    {
      source.delete();
    }
    System.err.println("finished compressing ");
  }

  /**
   * @param destination
   * @param buffSize
   * @param type
   * @return
   * @throws IOException
   */
  private CompressionWriter compressionFactory(final File destination, final int buffSize, final TYPE type) throws IOException
  {
    Writer result;

    switch (type)
    {
      // case BZIP:
      // result = new BZIPWriter(new FileOutputStream(destination),
      // buffSize);
      // break;
      case GZIP:
        result = new GZIPWriter(new FileOutputStream(destination), buffSize);
        break;
      case DEFLATE:
        result = new DeflateWriter(new FileOutputStream(destination), buffSize);
        break;
      default:
        result = null;
        break;
    }

    return result;
  }

  /**
   * <p>compressFile.</p>
   *
   * @param source a {@link java.lang.String} object.
   * @throws java.lang.Exception if any.
   */
  public void compressFile(final String source) throws Exception
  {

    compressFile(new File(source), new File(source + "." + getFileExtension()));
  }

  /**
   * @return
   */
  private String getFileExtension()
  {
    String result;

    switch (type)
    {
      // case BZIP:
      // result = "bz2";
      // break;
      case GZIP:
        result = "gz";
        break;
      case DEFLATE:
        result = "deflate";
        break;
      default:
        result = "unknown";
        break;
    }

    return result;
  }

  /**
   * <p>main.</p>
   *
   * @param args an array of {@link java.lang.String} objects.
   * @throws java.lang.Exception if any.
   */
  public static void main(final String[] args) throws Exception
  {
    PatternLayout _layout = new PatternLayout("%d{[yyyy/MM/dd HH:mm:SSSS]} %C{1} %p %F:%M():%L  [thread %t] %m\n");
    String filename = "logs/all.log";
    String datePattern = "'.'yyyy-MM-dd";
    DailyRollingCompressingFileAppender x = new DailyRollingCompressingFileAppender(_layout, filename, datePattern);

    for (int i = 0; i < 1000 * 10; i++)
    {
      x.append(new LoggingEvent("nope", Category.getRoot(), new Date().getTime(), Level.ERROR, "test", null));
    }
    x.setCompressionType(TYPE.GZIP);
    x.compressFile(filename);
  }

  /**
   * <p>setCompressionType.</p>
   *
   * @param type
   *          the type to set
   */
  public void setCompressionType(final TYPE type)
  {
    this.type = type;
  }

  /**
   * <p>getCompressionType.</p>
   *
   * @return the type
   */
  public TYPE getCompressionType()
  {
    return type;
  }
}
